Cache-busting bundles with assetUrl
-
What you'll see
A developer publishes an updated JS or CSS bundle, but visitors keep running the old version — for as long as their browser keeps the cached copy, which can be up to a year — because the bundle URL never changed and the browser saw no reason to refetch it. The symptom is not just wrong styling: an old JS bundle running against new HTML causes functional breakage, and it clears itself only when the browser cache expires or the user hard-refreshes.
-
What's actually happening
Browsers cache static assets aggressively. If the URL of a bundle stays the same across a publish, the browser treats the cached copy as current and does not refetch it.
docly.assetUrl(path)solves this by appending?v=<version>to the path, where the version is the publication's last-modified time (the newest of the published folder and its app) formattedyyyyMMddHHmmss. The stamp is site-wide, not per file: it changes whenever anything in the published folder or in its app is saved. There is no separate publish step — a save is live.Without it, a static file is not revalidated either. Images, CSS and JS files are served with
Cache-Control: max-ageof one year, so a changed file reaches a returning visitor only when the cached copy expires.The server backs this up: any response whose URL carries a
?v=query is served withCache-Control: max-age=31536000, immutable(one year). So an unchanged bundle is never refetched unnecessarily, and a changed bundle gets a brand-new URL after the next save and is refetched immediately. Public HTML without?v=is servedno-cacheinstead, so it revalidates cheaply (a 304 via Last-Modified). External URLs (containing://or starting with//) are returned unchanged, with no?v=.A
Cache-Controlheader you set yourself inheaders.json(or theheadersblock ofsite.json) replaces this policy for pages and files. API responses always stayno-cache. -
What to do
Wrap all of your own
<script src>and<link href>references indocly.assetUrl(...):<script src="#docly.assetUrl('/assets/js/bundle.js')#"></script> <link href="#docly.assetUrl('/assets/css/site.css')#" rel="stylesheet">The bundle now gets
?v=<version>. A save means a new version, a new URL, and an immediate refetch by the browser. The server setsimmutable, max-age=1 yearon?v=URLs, so unchanged bundles are never refetched unnecessarily — this also scores full marks on Google's "efficient cache policy" audit.⚠ Does NOT work for your own dynamic output.
assetUrlversions by save time (the publication's last-modified time), not by the actual content of a dynamically generated response. So do not useassetUrlon a URL that produces content which varies without a save:- output that depends on query parameters, the logged-in user, the current time, counters, or external sources
- anything that must be fresh per request
Why it is dangerous: a
?v=URL getsimmutable, max-age=1 year. If the browser or CDN caches the first dynamic response, that same response is served to everyone for a whole year — stale content, and potentially one user's data leaking to others.What to do instead: serve dynamic or per-user content from a
.jsAPI endpoint under#/API/. API responses are alwaysno-cacheand must not be versioned withassetUrl. If you need other caching headers on a response, note thatdocly.setHeader()exists only in#/API/scripts; a page or bundle gets its headers fromheaders.json. Fetch them from the browser at runtime.Rule of thumb:
- Static bundle/asset (changes only when you save it) → use assetUrl.
- Dynamic / per-user / per-request output → do not — serve it as a
.jsAPI (no-cache). - External URL (CDN etc.) → no — assetUrl leaves it unchanged anyway.
Last reviewed 17 Oct 2026